跳到主要内容

两种 Provider 的使用

你对 UI Automation 的 Client-Side Provider 和 Server-Side Provider 的了解非常正确。这两种方法确实可以为不支持 UI Automation 的应用添加自动化支持。要直接通过 COM 实现这些 Provider,需要一些复杂的步骤。我会尽量简化解释,并给出一些指导。

首先,让我们区分这两种 Provider:

  1. Client-Side Provider:

    • 运行在客户端进程中
    • 不需要修改目标应用程序
    • 通常用于为旧的或第三方应用程序添加 UI Automation 支持
  2. Server-Side Provider:

    • 运行在目标应用程序的进程中
    • 需要修改目标应用程序的源代码
    • 提供更精确的控制和更好的性能

实现步骤:

  1. 创建 COM 对象: 首先,你需要创建一个 COM 对象,实现相关的 UI Automation Provider 接口。

  2. 实现必要的接口: 根据你要支持的 UI 元素类型,实现相应的接口。例如:

    • IRawElementProviderSimple
    • IRawElementProviderFragment
    • 特定控件类型的接口(如 IValueProvider, IInvokeProvider 等)
  3. 注册 COM 对象: 将你的 COM 对象注册到系统中,使 UI Automation 能够找到并使用它。

  4. 连接到目标应用:

    • 对于 Client-Side Provider,你需要找到目标窗口并附加你的 Provider。
    • 对于 Server-Side Provider,你需要在应用程序中创建和公开你的 Provider。

示例代码(C++):

这里是一个非常简化的 Client-Side Provider 示例:

#include <UIAutomationCore.h>
#include <atlbase.h>
#include <atlcom.h>

// 声明 COM 类
class ATL_NO_VTABLE CMyProvider :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CMyProvider, &CLSID_MyProvider>,
public IRawElementProviderSimple,
public IRawElementProviderFragment
{
public:
DECLARE_NOT_AGGREGATABLE(CMyProvider)
BEGIN_COM_MAP(CMyProvider)
COM_INTERFACE_ENTRY(IRawElementProviderSimple)
COM_INTERFACE_ENTRY(IRawElementProviderFragment)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()

// IRawElementProviderSimple 方法
STDMETHOD(get_ProviderOptions)(ProviderOptions *pRetVal);
STDMETHOD(GetPatternProvider)(PATTERNID patternId, IUnknown **pRetVal);
STDMETHOD(GetPropertyValue)(PROPERTYID propertyId, VARIANT *pRetVal);
STDMETHOD(get_HostRawElementProvider)(IRawElementProviderSimple **pRetVal);

// IRawElementProviderFragment 方法
STDMETHOD(Navigate)(NavigateDirection direction, IRawElementProviderFragment **pRetVal);
STDMETHOD(GetRuntimeId)(SAFEARRAY **pRetVal);
STDMETHOD(get_BoundingRectangle)(UiaRect *pRetVal);
STDMETHOD(GetEmbeddedFragmentRoots)(SAFEARRAY **pRetVal);
STDMETHOD(SetFocus)();
STDMETHOD(get_FragmentRoot)(IRawElementProviderFragmentRoot **pRetVal);

// 其他方法...
};

OBJECT_ENTRY_AUTO(__uuidof(MyProvider), CMyProvider)

// 实现方法...
STDMETHODIMP CMyProvider::get_ProviderOptions(ProviderOptions *pRetVal)
{
*pRetVal = ProviderOptions_ClientSideProvider;
return S_OK;
}

// 实现其他方法...

要使用这个 Provider:

  1. 编译成 DLL 并注册为 COM 服务器。
  2. 在客户端代码中,找到目标窗口并创建 Provider 实例。
  3. 使用 UiaHostProviderFromHwnd 将 Provider 附加到窗口。
CComPtr<IRawElementProviderSimple> pProvider;
pProvider.CoCreateInstance(CLSID_MyProvider);

HWND hwndTarget = FindWindow(NULL, L"Target Window");
UiaHostProviderFromHwnd(hwndTarget, &pProvider);

注意事项:

  1. 这只是一个非常基础的示例。实际实现要复杂得多,需要处理更多细节。
  2. 直接使用 COM 实现 UI Automation Provider 需要深入了解 COM 编程和 UI Automation 框架。
  3. 对于 Java 或 Node.js 开发者,直接实现 COM 接口可能会很困难。你可能需要考虑使用 C++ 实现核心功能,然后通过 JNI (Java) 或 N-API (Node.js) 进行封装。

如果你主要使用 Java 或 Node.js,可能更适合使用现有的自动化库,如 Java 的 WindowsAccessBridge 或 Node.js 的 windows-automation 模块,这些库已经封装了许多底层细节。

不需要 Hook

你的理解是正确的。UiaHostProviderFromHwnd 函数确实不需要 hook 目标程序就能直接使用。这是 UI Automation 框架提供的一个强大特性,允许我们为现有的应用程序添加自动化支持,而无需修改目标程序的代码或注入任何东西到目标进程中。

让我详细解释一下 UiaHostProviderFromHwnd 的工作原理和使用方式:

  1. 功能: UiaHostProviderFromHwnd 允许你为指定的窗口句柄(HWND)关联一个自定义的 UI Automation provider。

  2. 工作原理:

    • 它在调用进程中创建一个 provider 到窗口的关联。
    • 当 UI Automation 客户端查询该窗口时,你的自定义 provider 会被调用,而不是默认的 provider。
  3. 使用场景:

    • 为不支持 UI Automation 的旧应用程序添加自动化支持。
    • 为第三方应用程序扩展或自定义自动化功能。
  4. 不需要 Hook: 这个函数不需要修改目标程序或向其注入代码。它完全在调用进程中运行。

  5. 使用示例:

HRESULT AddAutomationSupport(HWND hwndTarget)
{
// 创建自定义 provider
CComPtr<IRawElementProviderSimple> pProvider;
HRESULT hr = pProvider.CoCreateInstance(CLSID_MyCustomProvider);
if (FAILED(hr))
return hr;

// 将 provider 关联到目标窗口
hr = UiaHostProviderFromHwnd(hwndTarget, pProvider);
if (FAILED(hr))
return hr;

return S_OK;
}
  1. 注意事项:

    • provider 必须实现 IRawElementProviderSimple 接口。
    • provider 通常还需要实现其他接口,如 IRawElementProviderFragment,以提供完整的功能。
    • 这个方法创建的是一个 client-side provider,它运行在调用进程中,而不是目标应用程序进程中。
  2. 优点:

    • 非侵入性:不需要修改目标程序。
    • 灵活性:可以为任何窗口添加或自定义自动化支持。
    • 安全:不涉及注入代码到其他进程。
  3. 限制:

    • 由于 provider 运行在调用进程中,它可能无法访问目标应用程序的内部状态。
    • 性能可能不如直接在目标应用程序中实现的 server-side provider。
  4. 适用于 Java 和 Node.js: 虽然 UiaHostProviderFromHwnd 是一个 Windows API 函数,主要用于 C/C++ 开发,但是:

    • 对于 Java,你可以使用 JNA (Java Native Access) 来调用这个函数。
    • 对于 Node.js,可以使用 node-ffi 模块来实现类似的功能。

总的来说,UiaHostProviderFromHwnd 提供了一种强大而灵活的方式来增强现有应用程序的自动化能力,而无需修改目标程序。这对于开发自动化工具、辅助技术或测试框架特别有用。

UiaHostProviderFromHwnd 的原理

你提出了一个非常好的问题。确实,UiaHostProviderFromHwnd 的工作原理可能看起来有点神奇。让我详细解释一下它是如何工作的,以及为什么它能为没有实现 UI Automation 的程序提供这种能力。

UiaHostProviderFromHwnd 的工作原理:

  1. Windows 消息系统:

    • Windows 使用消息系统来管理窗口和控件。
    • 每个窗口都有一个消息队列和一个窗口过程(Window Procedure)来处理消息。
  2. UI Automation 的系统级集成:

    • UI Automation 在系统级别集成到了 Windows 的消息系统中。
    • 当 UI Automation 客户端请求信息时,系统会向目标窗口发送特定的消息。
  3. UiaHostProviderFromHwnd 的角色:

    • 这个函数告诉系统,对于指定的窗口句柄(HWND),应该使用提供的自定义 Provider 来响应 UI Automation 请求。
    • 它实际上是在系统级别注册了一个回调机制。
  4. 消息拦截:

    • 当 UI Automation 相关的消息被发送到目标窗口时,系统会先检查是否有注册的自定义 Provider。
    • 如果有,系统会调用这个自定义 Provider,而不是将消息直接发送到目标窗口。
  5. 无需目标程序配合:

    • 重要的是,这整个过程不需要目标程序的任何特殊处理或修改。
    • 自定义 Provider 完全在调用进程中运行,不会影响到目标程序的代码执行。
  6. 限制和边界:

    • 自定义 Provider 无法直接访问目标程序的内部状态。
    • 它主要基于可以从外部观察到的窗口属性和行为来提供信息。

示例说明:

想象你有一个旧的计算器程序,它没有实现 UI Automation。你可以创建一个自定义 Provider:

class CalculatorProvider : public IRawElementProviderSimple 
{
public:
HRESULT GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override
{
if (propertyId == UIA_NamePropertyId)
{
pRetVal->vt = VT_BSTR;
pRetVal->bstrVal = SysAllocString(L"Calculator");
return S_OK;
}
// 处理其他属性...
return S_OK;
}

// 实现其他必要的接口方法...
};

// 使用方法
HWND hwndCalculator = FindWindow(L"CalculatorClass", NULL);
CComPtr<IRawElementProviderSimple> pProvider = new CalculatorProvider();
UiaHostProviderFromHwnd(hwndCalculator, pProvider);

这段代码做了以下几件事:

  1. 创建了一个自定义 Provider,它知道如何描述计算器窗口。
  2. 找到计算器窗口的句柄。
  3. 使用 UiaHostProviderFromHwnd 将自定义 Provider 与计算器窗口关联。

现在,当 UI Automation 客户端查询这个计算器窗口时:

  1. 系统会发现有一个自定义 Provider 注册在这个窗口上。
  2. 系统会调用你的 CalculatorProvider 来获取信息,而不是直接查询计算器程序。
  3. 你的 Provider 可以提供你认为合适的信息,即使原始程序并不支持 UI Automation。

总结: UiaHostProviderFromHwnd 允许你"拦截"对特定窗口的 UI Automation 查询,并用你自己的实现来响应这些查询。这是一种非侵入式的方法,可以为不支持 UI Automation 的应用程序添加辅助功能支持,而无需修改原始程序。

如何实现

意思是目标程序本身是需要提供这个对外的 UI 信息访问,然后我自己实现的 Provider 实际上就是对这个本来就能采集到的信息转换成 UI Automation 能识别的信息吗

你的理解接近了真相,但还需要一些澄清。让我详细解释一下:

  1. 目标程序的基本信息:

    • 所有 Windows 程序,即使没有专门实现 UI Automation,也会通过 Windows API 提供一些基本的 UI 信息。
    • 这些信息包括窗口标题、位置、大小、可见性等。
  2. Windows 消息系统:

    • Windows 使用消息系统来管理窗口交互。
    • 即使程序没有实现 UI Automation,它也会响应某些标准的 Windows 消息。
  3. 你的自定义 Provider 的角色:

    • 你的 Provider 可以利用这些基本信息和标准消息来构建更丰富的 UI Automation 接口。
    • 它不仅仅是简单地转换已有信息,而是可以通过各种方法来"推断"或"模拟"更多的信息。
  4. 信息获取方法:

    • 使用 Windows API 函数(如 GetWindowText, GetWindowRect 等)获取基本信息。
    • 发送 Windows 消息(如 WM_GETTEXT)来获取更多细节。
    • 分析窗口的视觉结构来推断控件类型和层次。
  5. 增强和扩展:

    • 你的 Provider 可以添加原本不存在的功能,比如为不支持键盘导航的程序添加这个功能。
    • 可以实现更高级的模式,如模式识别来猜测控件的用途。
  6. 限制:

    • 你无法直接访问目标程序的内部数据结构或逻辑。
    • 某些复杂的交互或自定义控件可能难以准确模拟。

示例:

假设有一个简单的旧式文本编辑器,它没有实现 UI Automation。你的 Provider 可能会这样工作:

class TextEditorProvider : public IRawElementProviderSimple 
{
public:
HRESULT GetPropertyValue(PROPERTYID propertyId, VARIANT *pRetVal) override
{
switch (propertyId)
{
case UIA_NamePropertyId:
// 使用 Windows API 获取窗口标题
TCHAR title[256];
GetWindowText(m_hwnd, title, 256);
pRetVal->bstrVal = SysAllocString(title);
pRetVal->vt = VT_BSTR;
return S_OK;

case UIA_BoundingRectanglePropertyId:
// 获取窗口位置和大小
RECT rect;
GetWindowRect(m_hwnd, &rect);
// 填充 UiaRect 结构...
return S_OK;

case UIA_IsTextPatternAvailablePropertyId:
// 假设这是一个文本编辑器,我们声明它支持文本模式
pRetVal->boolVal = VARIANT_TRUE;
pRetVal->vt = VT_BOOL;
return S_OK;
}
return E_NOTIMPL;
}

// 实现 ITextProvider 接口来提供文本访问能力
HRESULT GetText(int startOffset, int endOffset, BSTR *pRetVal) override
{
// 发送 WM_GETTEXT 消息来获取文本内容
int length = SendMessage(m_hwnd, WM_GETTEXTLENGTH, 0, 0);
std::vector<TCHAR> buffer(length + 1);
SendMessage(m_hwnd, WM_GETTEXT, length + 1, (LPARAM)buffer.data());

// 处理 startOffset 和 endOffset...
*pRetVal = SysAllocString(buffer.data());
return S_OK;
}

private:
HWND m_hwnd; // 目标窗口的句柄
};

在这个例子中:

  1. Provider 使用标准 Windows API 获取基本窗口信息。
  2. 它假设窗口是文本编辑器,并声明支持文本模式。
  3. 通过发送 Windows 消息来获取文本内容,即使程序本身没有实现 UI Automation。

总结: 你的 Provider 不仅仅是转换已有信息,而是主动使用各种 Windows API 和消息来获取信息,并将这些基本信息转化为更丰富的 UI Automation 接口。它可以添加原程序没有的功能,但也受限于无法直接访问程序内部状态。这种方法允许你为旧程序或第三方应用添加辅助功能支持,而无需修改原始代码。

如何分析程序的 UI 信息

你提出了一个非常深刻的问题,这触及了 UI 自动化和应用程序分析的核心。让我详细解释一下这个问题:

  1. 基本 Windows 信息

所有 Windows 程序,无论是否支持 UI Automation,都会通过 Windows API 提供一些基本信息:

  • 窗口句柄 (HWND)
  • 窗口类名
  • 窗口标题
  • 窗口位置和大小
  • 子窗口和控件

这些信息可以通过标准 Windows API 函数获取,如 GetWindowText(), GetClassName(), GetWindowRect() 等。

  1. 控件类型推断

虽然程序可能不直接支持 UI Automation,但我们可以通过分析窗口类名和其他属性来推断控件类型:

  • "Button" 类通常是按钮
  • "Edit" 类通常是文本框
  • "ListBox" 类通常是列表框
  1. 消息响应分析

通过发送 Windows 消息并分析响应,我们可以推断控件的功能:

  • 发送 WM_GETTEXT 消息可以获取控件的文本内容
  • 发送 WM_GETTEXTLENGTH 可以获取文本长度
  • 尝试 EM_GETSEL 消息可以检测是否支持文本选择
  1. 视觉分析

通过分析窗口的视觉结构,我们可以推断 UI 元素的层次和关系:

  • 使用 GetWindowRect() 和 GetClientRect() 分析控件的位置和大小
  • 分析子窗口的排列可以推断出布局结构(如表格、列表等)
  1. 行为分析

通过模拟用户操作并观察结果,我们可以推断控件的行为:

  • 发送鼠标点击消息并观察窗口变化
  • 尝试设置焦点并观察响应
  1. 启发式方法

基于常见的 UI 模式和最佳实践,我们可以做出一些合理的假设:

  • 窗口顶部的子窗口可能是菜单栏
  • 窗口底部的进度条可能表示某种进度
  • 特定位置的图标可能代表特定功能(如关闭、最小化按钮)
  1. 工具和技术

有一些工具和技术可以帮助分析应用程序的 UI 结构:

  • Spy++ (包含在 Visual Studio 中) 可以查看窗口层次和属性
  • Windows SDK 中的 Inspect.exe 工具
  • 使用 Windows Hook 技术来拦截和分析消息

示例代码:

以下是一个简单的例子,展示如何获取基本窗口信息并推断一些 UI 元素:

#include <windows.h>
#include <iostream>
#include <string>

void AnalyzeWindow(HWND hwnd) {
char className[256];
char windowText[256];
RECT rect;

GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);
GetWindowRect(hwnd, &rect);

std::cout << "Class: " << className << std::endl;
std::cout << "Text: " << windowText << std::endl;
std::cout << "Position: (" << rect.left << "," << rect.top << ") - ("
<< rect.right << "," << rect.bottom << ")" << std::endl;

// 推断控件类型
if (strcmp(className, "Button") == 0) {
std::cout << "This appears to be a button." << std::endl;
} else if (strcmp(className, "Edit") == 0) {
std::cout << "This appears to be a text box." << std::endl;

// 检查是否支持文本选择
LRESULT result = SendMessage(hwnd, EM_GETSEL, 0, 0);
if (result != -1) {
std::cout << "This text box supports text selection." << std::endl;
}
}

// 检查子窗口
HWND child = GetWindow(hwnd, GW_CHILD);
if (child) {
std::cout << "This window has child windows." << std::endl;
}
}

int main() {
HWND hwnd = FindWindow(NULL, "Notepad"); // 查找记事本窗口
if (hwnd) {
AnalyzeWindow(hwnd);
} else {
std::cout << "Notepad window not found." << std::endl;
}
return 0;
}

这个例子展示了如何获取基本窗口信息,并做一些简单的推断。在实际应用中,你可能需要更复杂的逻辑和更多的启发式方法来准确分析 UI 结构。

总结: 虽然目标程序可能不直接支持 UI Automation,但通过综合使用 Windows API、消息系统、视觉分析和启发式方法,我们可以收集足够的信息来构建一个合理的 UI 模型。这种方法虽然不如直接支持 UI Automation 的程序那样精确和全面,但对于许多自动化和辅助技术应用来说已经足够有用了。

可以使用的 UI 分析工具

是的,有几种方法可以分析应用程序窗口内的子UI元素。以下是一些常用的技术:

  1. 枚举子窗口

使用 Windows API 函数 EnumChildWindows() 可以枚举一个窗口的所有直接子窗口。

BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam) {
char className[256];
char windowText[256];
GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);

printf("Child Window - Class: %s, Text: %s\n", className, windowText);
return TRUE;
}

// 使用
EnumChildWindows(parentHwnd, EnumChildProc, 0);
  1. 递归枚举

通过递归调用,可以枚举出窗口的整个子窗口树结构。

void EnumChildWindowsRecursive(HWND hwnd, int level = 0) {
char className[256];
char windowText[256];
GetClassName(hwnd, className, 256);
GetWindowText(hwnd, windowText, 256);

printf("%*s%s: %s\n", level*2, "", className, windowText);

// 递归枚举子窗口
HWND child = GetWindow(hwnd, GW_CHILD);
while (child) {
EnumChildWindowsRecursive(child, level + 1);
child = GetWindow(child, GW_HWNDNEXT);
}
}
  1. 使用 Accessibility API

Windows 提供了 Accessibility API,可以获取更详细的UI元素信息,包括那些不是窗口的UI元素。

#include <oleacc.h>

void EnumAccessibleChildren(IAccessible* pAcc, int level = 0) {
long childCount;
pAcc->get_accChildCount(&childCount);

for (long i = 1; i <= childCount; i++) {
VARIANT varChild;
varChild.vt = VT_I4;
varChild.lVal = i;

IDispatch* pDisp;
pAcc->get_accChild(varChild, &pDisp);

if (pDisp) {
IAccessible* pChildAcc;
pDisp->QueryInterface(IID_IAccessible, (void**)&pChildAcc);

if (pChildAcc) {
BSTR bstrName;
pChildAcc->get_accName(varChild, &bstrName);

VARIANT varRole;
pChildAcc->get_accRole(varChild, &varRole);

printf("%*sName: %S, Role: %d\n", level*2, "", bstrName, varRole.lVal);

SysFreeString(bstrName);
EnumAccessibleChildren(pChildAcc, level + 1);
pChildAcc->Release();
}
pDisp->Release();
}
}
}

// 使用
IAccessible* pAcc;
HRESULT hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, IID_IAccessible, (void**)&pAcc);
if (SUCCEEDED(hr)) {
EnumAccessibleChildren(pAcc);
pAcc->Release();
}
  1. 使用 UI Automation API

对于支持 UI Automation 的应用程序,可以使用 UI Automation API 来获取更丰富的UI元素信息。

#include <UIAutomation.h>

void EnumUiAutomationElements(IUIAutomationElement* pElement, int level = 0) {
BSTR name;
pElement->get_CurrentName(&name);

CONTROLTYPEID controlType;
pElement->get_CurrentControlType(&controlType);

printf("%*sName: %S, ControlType: %d\n", level*2, "", name, controlType);
SysFreeString(name);

IUIAutomationTreeWalker* pTreeWalker;
g_pAutomation->get_ControlViewWalker(&pTreeWalker);

IUIAutomationElement* pChild;
pTreeWalker->GetFirstChildElement(pElement, &pChild);

while (pChild) {
EnumUiAutomationElements(pChild, level + 1);
IUIAutomationElement* pNext;
pTreeWalker->GetNextSiblingElement(pChild, &pNext);
pChild->Release();
pChild = pNext;
}

pTreeWalker->Release();
}

// 使用
IUIAutomation* g_pAutomation;
CoCreateInstance(CLSID_CUIAutomation, NULL, CLSCTX_INPROC_SERVER,
IID_IUIAutomation, (void**)&g_pAutomation);

IUIAutomationElement* pRoot;
g_pAutomation->ElementFromHandle(hwnd, &pRoot);
EnumUiAutomationElements(pRoot);
pRoot->Release();
g_pAutomation->Release();
  1. 使用第三方工具

有一些第三方工具可以帮助分析应用程序的UI结构,例如:

  • Spy++ (包含在Visual Studio中)
  • Inspect.exe (Windows SDK工具)
  • UISpy (开源工具)

这些方法各有优缺点:

  • 枚举子窗口方法简单,但只能获取窗口级别的元素。
  • Accessibility API 可以获取更多元素,但可能不够详细。
  • UI Automation API 提供最丰富的信息,但并非所有应用程序都完全支持它。

在实际应用中,你可能需要结合使用这些方法来获取最全面的UI元素信息。对于不同的目标应用程序,可能需要尝试不同的方法来找出最有效的分析技术。